/*
 * Copyright (c) 2011 Imre Fazekas.
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * Neither the name of the Brillien nor the names of its
 * terms and concepts may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package com.vii.brillien.core.management;


import com.vii.brillien.core.management.db.couchdbDocument.IndispensableFlow;
import com.vii.brillien.core.management.db.couchdbDocument.NodePresence;
import com.vii.brillien.core.management.db.couchdbDocument.ThreadDocument;
import com.vii.brillien.ignition.BrillienContext;
import com.vii.brillien.ignition.transport.BrillienCommunication;
import com.vii.streamline.services.MathServices;
import org.jcouchdb.db.Database;
import org.jcouchdb.db.Options;
import org.jcouchdb.db.ServerImpl;
import org.jcouchdb.document.DesignDocument;
import org.jcouchdb.document.ValueRow;
import org.jcouchdb.document.ViewResult;
import org.jcouchdb.util.CouchDBUpdater;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;

import static com.vii.brillien.ignition.BrillienContext.*;
import static com.vii.brillien.ignition.BrillienStatics.COUCH_DESIGN_DIR;

/**
 * Service class to handle couchdb related operations
 */
public class CouchDBServices {

    private static ServerImpl           server;
    private static Database             database;

    private static String               SERVER_ADDRESS = "localhost";
    private static String               DB_NAME = "brillien_board";
  
    private static long                 lastReport;
    private static List<NodePresence>   nodes;

    static{
        nodes = new LinkedList<NodePresence>();
    }

    /**
     * initial method opening database and synchronizing design documents
     */
    public static boolean timeToRelax(){
        if( !BrillienContext.COUCHDB_BOARD ) return false;

        SERVER_ADDRESS = COUCHDB_URI;
        //DB_NAME = SERVER_NAME.toLowerCase() + "_" + DB_NAME;
        BrillienContext.systemLog( Level.FINE, " CouchDB Server Address :::  " + SERVER_ADDRESS );
        BrillienContext.systemLog( Level.FINE, " CouchDB Server Name :::  " + SERVER_NAME );
        BrillienContext.systemLog( Level.FINE, " CouchDB DB Name :::  " + DB_NAME );

        server = new ServerImpl( SERVER_ADDRESS );

        if( CLEAN_AT_STARTUP ){
            BrillienContext.systemLog( Level.FINE, " Cleaning CouchDB..." );
            try{
                server.deleteDatabase( DB_NAME );
            } catch(Exception e){
                BrillienContext.systemLog( Level.FINE, " Unsuccessful DB cleaning..." );
            }
        }

        BrillienContext.systemLog( Level.FINE, " Creating Database:" + DB_NAME );
        server.createDatabase( DB_NAME );

        database = new Database( server, DB_NAME );

        BrillienContext.systemLog( Level.FINE, " Updating design documents..." );
        CouchDBUpdater updater = new CouchDBUpdater();
        updater.setDatabase( database );
        updater.setDesignDocumentDir( new File( COUCH_DESIGN_DIR ) );

        try {
            List<DesignDocument> docs = updater.updateDesignDocuments();
            for( DesignDocument dd : docs ){
                ;
            }
        } catch (IOException e) {
            exceptionLog( CouchDBServices.class.getName(), "timeToRelax", e );
        }

        return true;
    }

    /**
     * finalize method to close couchdb-related functionality
     */
    public static boolean closeCouch(){

        if( !BrillienContext.COUCHDB_BOARD || database == null )
            return false;

        //server.destroy();

        return true;
    }

    private static boolean addNewNodePresence(){
        NodePresence bd = new NodePresence();
        bd.setServerName( SERVER_NAME );
        bd.setServerVersion( SERVER_VERSION );
        bd.setJmxServicePort( JMX_SERVICE_PORT );
        bd.setJmxServiceOn( JMX_SERVICE_ON );
        bd.setLastReport( System.currentTimeMillis() );

        database.createOrUpdateDocument( bd );

        return true;
    }

    /**
     * Updates of the hearbeat event document of the running Brillien node to the couchDB
     */
    public static synchronized boolean heartBeat(){
        if( !BrillienContext.COUCHDB_BOARD || database == null ) 
            return MessageControlCenter.heartBeat();

        ViewResult<NodePresence> view = database.queryView( DB_NAME + "/node_presence", NodePresence.class, null, null);

        boolean found = false;
        nodes.clear();

        for( ValueRow<NodePresence> vr : view.getRows() ){
            if( vr.getValue().getServerName().equals( SERVER_NAME ) ){
                found = true;
                vr.getValue().setLastReport( lastReport = System.currentTimeMillis() );
                database.updateDocument( vr.getValue() );
            }
            else{
                if( System.currentTimeMillis() - vr.getValue().getLastReport() < HEARTBEAT_PATIENCE )
                    nodes.add( vr.getValue() );
            }
        }

        if( !found )
            addNewNodePresence();

        return true;
    }

    /**
     * Collects the documents of all possible failed flows
     */
    public static IndispensableFlow getSuspendedFlowStateSpaceByID( String flowID ) {
        BrillienContext.systemLog( Level.FINEST, "getSuspendedFlowStateSpaceByID", flowID );

        if( !BrillienContext.COUCHDB_BOARD || database == null )
            return MessageControlCenter.getSuspendedFlowStateSpaceByID( flowID );

        ViewResult<IndispensableFlow> view = database.queryView( DB_NAME + "/collect_suspended_flows", IndispensableFlow.class, new Options().key(flowID), null);
        for( ValueRow<IndispensableFlow> vr : view.getRows() )
            return vr.getValue();
        return null;
    }

    public static boolean refreshIndispensableFlow( IndispensableFlow ifd ) {
        BrillienContext.systemLog( Level.FINEST, "refreshIndispensableFlow", ifd.getActivationMessage() );

        if( !BrillienContext.COUCHDB_BOARD || database == null )
            return MessageControlCenter.refreshIndispensableFlow( ifd );

        database.updateDocument( ifd );

        return true;
    }

    /**
     * Collects the documents of all possible failed flows
     */
    public static List<IndispensableFlow> getLostFlows() { List<IndispensableFlow> flows = new LinkedList<IndispensableFlow>();
        BrillienContext.systemLog( Level.FINEST, "getLostFlows" );

        if( !BrillienContext.COUCHDB_BOARD || database == null )
            return MessageControlCenter.getLostFlows();

        ViewResult<IndispensableFlow> view = database.queryView( DB_NAME + "/collect_unfinished_flows", IndispensableFlow.class, new Options().key( SERVER_NAME ), null);
        for( ValueRow<IndispensableFlow> vr : view.getRows() ){
            IndispensableFlow idf = vr.getValue();
            if( idf.getStartDate() + idf.getTimeout() + FLOW_PATIENCE < System.currentTimeMillis() )
                flows.add( vr.getValue() );
        }
        return flows;
    }

    public static boolean reportChangedState( String flowID, HashMap<String, Object> state ){
        BrillienContext.systemLog( Level.FINEST, "reportChangedState", flowID, state );

        if( !BrillienContext.COUCHDB_BOARD || database == null )
            return MessageControlCenter.reportChangedState( flowID, state );

        try{
            IndispensableFlow ifd = database.getDocument(IndispensableFlow.class, flowID);
            ifd.setFlowStateSpace( state );
            database.updateDocument( ifd );
        }catch(Exception e){
            exceptionLog(CouchDBServices.class.getName(), "reportChangedState", e);
        }

        return true;
    }

    public static boolean reportFlowSuspense( String flowID, HashMap<String, Object> state ){
        BrillienContext.systemLog( Level.FINEST, "reportFlowSuspense", flowID, state );

        if( !BrillienContext.COUCHDB_BOARD || database == null )
            return MessageControlCenter.reportFlowSuspense( flowID, state );

        try{
            IndispensableFlow ifd = database.getDocument(IndispensableFlow.class, flowID);
            ifd.setFlowStateSpace( state );
            ifd.setStatus( IndispensableFlow.FLOW_SUSPENDED );
            database.updateDocument( ifd );
        }catch(Exception e){
            exceptionLog(CouchDBServices.class.getName(), "reportFlowSuspense", e);
        }

        return true;
    }

    public static boolean reportFlowReactivation( String flowID ){
        BrillienContext.systemLog( Level.FINEST, "reportFlowReactivation", flowID );

        if( !BrillienContext.COUCHDB_BOARD || database == null )
            return MessageControlCenter.reportFlowReactivation( flowID );

        try{
            IndispensableFlow ifd = database.getDocument(IndispensableFlow.class, flowID);
            ifd.setStatus( IndispensableFlow.FLOW_STARTED );
            database.updateDocument( ifd );
        }catch(Exception e){
            exceptionLog(CouchDBServices.class.getName(), "reportFlowReactivation", e);
        }

        return true;
    }

    /**
     * Updates a flow's document with failed execution statement
     */
    public static boolean reportFlowFailure(String flowID, String message ) {
        BrillienContext.systemLog( Level.FINEST, "reportFlowFailure", flowID, message );

        if( !BrillienContext.COUCHDB_BOARD || database == null )
            return MessageControlCenter.reportFlowFailure( flowID, message );

        try{
            IndispensableFlow ifd = database.getDocument(IndispensableFlow.class, flowID);
            ifd.setStatus( IndispensableFlow.FLOW_FAILED );
            ifd.setFailureMessage( message );
            database.updateDocument( ifd );
        }catch(Exception e){
            exceptionLog(CouchDBServices.class.getName(), "reportFlowTermination", e);
        }

        return true;
    }

    /**
     * Updates a flow's document with success execution statement
     */
    public static boolean reportFlowTermination(String flowID) {
        BrillienContext.systemLog( Level.FINEST, "reportFlowTermination", flowID );

        if( !BrillienContext.COUCHDB_BOARD || database == null )
            return MessageControlCenter.reportFlowTermination( flowID );

        try{
            IndispensableFlow ifd = database.getDocument(IndispensableFlow.class, flowID);
            ifd.setStatus( IndispensableFlow.FLOW_FINISHED );
            database.updateDocument( ifd );
        }catch(Exception e){
            exceptionLog(CouchDBServices.class.getName(), "reportFlowTermination", e);
        }

        return true;
    }

    public static synchronized String getDelegationNode(){
        return nodes.size() == 0
            ? SERVER_NAME
            : nodes.get( (int)(MathServices.random.nextDouble() * nodes.size()) ).getServerName();
    }

    /**
     * Creates a document describing the indispensable flow just started
     */
    public static boolean flowStarted(String flowID, String name, HashMap<String, Object> state, long timeout, BrillienCommunication message ) throws IOException {
        BrillienContext.systemLog( Level.FINEST, "flowStarted", flowID, name, state, timeout );

        if( !BrillienContext.COUCHDB_BOARD || database == null )
            return MessageControlCenter.flowStarted( flowID, name, state, timeout, message );

        IndispensableFlow ifd = new IndispensableFlow();
        ifd.setId( flowID );
        ifd.setFlowName( name );
        ifd.setFlowStateSpace( state );
        ifd.setStatus( IndispensableFlow.FLOW_STARTED );
        ifd.setStartDate( System.currentTimeMillis() );
        ifd.setTimeout( timeout );
        ifd.setActivationMessage( message );
        ifd.setStartedBy( SERVER_NAME );

        ifd.setDelegatedTo(
            getDelegationNode()
        );

        database.createDocument( ifd );

        return true;
    }

}

